// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/pending_beacon_host.h"

#include <tuple>
#include <vector>

#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/pending_beacon_service.h"
#include "content/public/browser/child_process_termination_info.h"
#include "content/public/browser/permission_result.h"
#include "content/public/test/mock_permission_manager.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/system/functions.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/frame/pending_beacon.mojom-shared.h"
#include "third_party/blink/public/mojom/frame/pending_beacon.mojom.h"
#include "url/origin.h"

namespace content {
namespace {

constexpr char kBeaconTargetURL[] = "https://pending-beacon.test/send";
constexpr char kBeaconPageURL[] = "https://pending-beacon.test";

MATCHER_P2(VerifyResourceRequest,
           method,
           url,
           base::StrCat({"ResourceRequest is", negation ? " not" : "",
                         " matched"})) {
  const network::ResourceRequest& req = arg;
  if (req.mode != network::mojom::RequestMode::kCors) {
    *result_listener << "request mode must be CORS";
    return false;
  }
  if (req.request_initiator != url::Origin::Create(GURL(kBeaconPageURL))) {
    *result_listener << "request initiator must be " << kBeaconPageURL;
    return false;
  }
  if (req.credentials_mode != network::mojom::CredentialsMode::kSameOrigin) {
    *result_listener << "credentials mode must be Same-Origin";
    return false;
  }
  if (req.method != method) {
    return false;
  }
  if (req.url != url) {
    *result_listener << "expect url: " << url << ", got: " << req.url;
    return false;
  }
  if (method == net::HttpRequestHeaders::kPostMethod) {
    if (!req.keepalive) {
      *result_listener << "Post request must set keepalive";
      return false;
    }
  }
  return true;
}

}  // namespace

struct MockClientBeacon {
  MockClientBeacon(const MockClientBeacon&) = delete;
  MockClientBeacon& operator=(const MockClientBeacon&) = delete;
  MockClientBeacon() = default;

  void SendNow() {
    remote->SendNow();
    remote.FlushForTesting();
  }

  mojo::Remote<blink::mojom::PendingBeacon> remote;
};

class PendingBeaconHostTestBase : public RenderViewHostTestHarness {
 public:
  PendingBeaconHostTestBase(const PendingBeaconHostTestBase&) = delete;
  PendingBeaconHostTestBase& operator=(const PendingBeaconHostTestBase&) =
      delete;
  PendingBeaconHostTestBase() = default;

 protected:
  PendingBeaconHost* host() { return GetOrCreateHostIfNotExist(); }
  mojo::Remote<blink::mojom::PendingBeaconHost>& host_remote() {
    DCHECK(GetOrCreateHostIfNotExist());
    return host_remote_;
  }

  // Ask PendingBeaconHost to create `total` browser-side beacons.
  // Returns the mock client beacons that connect to browser-side beacons
  // The URLs for the beacons are generated by `CreateBeaconTargetURL()`.
  std::vector<MockClientBeacon> CreateBeacons(size_t total,
                                              const std::string& method) {
    GetOrCreateHostIfNotExist();
    std::vector<MockClientBeacon> client_beacons(total);
    for (size_t i = 0; i < total; i++) {
      host_remote_->CreateBeacon(
          client_beacons[i].remote.BindNewPipeAndPassReceiver(),
          CreateBeaconTargetURL(i), ToBeaconMethod(method));
    }
    host_remote_.FlushForTesting();
    return client_beacons;
  }
  std::unique_ptr<MockClientBeacon> CreateBeacon(const std::string& method) {
    return CreateBeacon(method, kBeaconTargetURL);
  }
  std::unique_ptr<MockClientBeacon> CreateBeacon(const std::string& method,
                                                 const std::string& url) {
    GetOrCreateHostIfNotExist();
    auto client_beacon = std::make_unique<MockClientBeacon>();
    host_remote_->CreateBeacon(
        client_beacon->remote.BindNewPipeAndPassReceiver(), GURL(url),
        ToBeaconMethod(method));
    host_remote_.FlushForTesting();
    return client_beacon;
  }

  static blink::mojom::BeaconMethod ToBeaconMethod(const std::string& method) {
    if (method == net::HttpRequestHeaders::kGetMethod) {
      return blink::mojom::BeaconMethod::kGet;
    }
    return blink::mojom::BeaconMethod::kPost;
  }

  static GURL CreateBeaconTargetURL(size_t i) {
    return GURL(base::StringPrintf("%s/%zu", kBeaconTargetURL, i));
  }

  // Verifies if the total number of network requests sent via
  // `test_url_loader_factory_` equals to `expected`.
  void ExpectTotalNetworkRequests(const base::Location& location,
                                  const int expected) {
    EXPECT_EQ(test_url_loader_factory_->NumPending(), expected)
        << location.ToString();
  }

  std::unique_ptr<BrowserContext> CreateBrowserContext() override {
    auto context = std::make_unique<TestBrowserContext>();
    context->SetPermissionControllerDelegate(
        std::make_unique<::testing::NiceMock<MockPermissionManager>>());
    return context;
  }

  // Updates the `permission_type` to the given `permission_status` through
  // the MockPermissionManager.
  void SetPermissionStatus(blink::PermissionType permission_type,
                           blink::mojom::PermissionStatus permission_status) {
    auto* mock_permission_manager = static_cast<MockPermissionManager*>(
        browser_context()->GetPermissionControllerDelegate());

    ON_CALL(*mock_permission_manager,
            GetPermissionResultForOriginWithoutContext(permission_type,
                                                       ::testing::_))
        .WillByDefault(::testing::Return(PermissionResult(
            permission_status, PermissionStatusSource::UNSPECIFIED)));
  }

  std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;

 private:
  // Returns an instance of PendingBeaconHost. Creates one if it does not exist.
  // The returned PendingBeaconHost uses a new instance of TestURLLoaderFactory
  // stored at `test_url_loader_factory_`.
  // The network requests made by the returned PendingBeaconHost will go through
  // `test_url_loader_factory_` which is useful for examining requests.
  PendingBeaconHost* GetOrCreateHostIfNotExist() {
    if (auto* host = PendingBeaconHost::GetForCurrentDocument(main_rfh())) {
      return host;
    }

    SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
                        blink::mojom::PermissionStatus::GRANTED);

    test_url_loader_factory_ =
        std::make_unique<network::TestURLLoaderFactory>();
    NavigateAndCommit(GURL(kBeaconPageURL));

    PendingBeaconHost::CreateForCurrentDocument(
        main_rfh(), test_url_loader_factory_->GetSafeWeakWrapper(),
        PendingBeaconService::GetInstance());
    auto* host = PendingBeaconHost::GetForCurrentDocument(main_rfh());
    host->SetReceiver(host_remote_.BindNewPipeAndPassReceiver());
    return host;
  }

  // Binds to the host from `GetOrCreateHostIfNotExist()`.
  mojo::Remote<blink::mojom::PendingBeaconHost> host_remote_;
};

class PendingBeaconHostTest
    : public PendingBeaconHostTestBase,
      public ::testing::WithParamInterface<std::string> {
 protected:
  void SetUp() override {
    const std::vector<base::test::FeatureRefAndParams> enabled_features = {
        {blink::features::kPendingBeaconAPI, {{"send_on_pagehide", "true"}}}};
    feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
    PendingBeaconHostTestBase::SetUp();
  }

  // Registers a callback to verify if the most-recent network request's content
  // matches the given `method` and `url`.
  void SetExpectNetworkRequest(const base::Location& location,
                               const std::string& method,
                               const GURL& url) {
    test_url_loader_factory_->SetInterceptor(base::BindLambdaForTesting(
        [this, location, method, url](const network::ResourceRequest& request) {
          if (has_verified_request_) {
            return;
          }
          has_verified_request_ = true;
          EXPECT_THAT(request, VerifyResourceRequest(method, url))
              << location.ToString();
        }));
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  bool has_verified_request_ = false;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    PendingBeaconHostTest,
    ::testing::Values(net::HttpRequestHeaders::kGetMethod,
                      net::HttpRequestHeaders::kPostMethod),
    [](const testing::TestParamInfo<PendingBeaconHostTest::ParamType>& info) {
      return info.param;
    });

TEST_P(PendingBeaconHostTest, SendBeacon) {
  const std::string method = GetParam();
  const auto url = GURL(kBeaconTargetURL);
  auto beacon = CreateBeacon(method);

  SetExpectNetworkRequest(FROM_HERE, method, url);
  beacon->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_P(PendingBeaconHostTest, SendOneOfBeacons) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Sends out only the 3rd of 5 created beacons.
  auto beacons = CreateBeacons(total, method);

  const size_t sent_beacon_i = 2;
  SetExpectNetworkRequest(FROM_HERE, method,
                          CreateBeaconTargetURL(sent_beacon_i));
  beacons[sent_beacon_i].SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_P(PendingBeaconHostTest, SendBeacons) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Sends out all 5 created beacons, in reversed order.
  auto beacons = CreateBeacons(total, method);
  for (int i = beacons.size() - 1; i >= 0; i--) {
    SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i));
    beacons[i].SendNow();
  }
  ExpectTotalNetworkRequests(FROM_HERE, total);
}

TEST_P(PendingBeaconHostTest, DeleteAndSendBeacon) {
  const std::string method = GetParam();
  const auto url = GURL(kBeaconTargetURL);
  auto beacon = CreateBeacon(method);
  auto& remote = beacon->remote;

  // Deleted beacon won't be sent out by host.
  remote->Deactivate();
  remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 0);
}

TEST_P(PendingBeaconHostTest, DeleteOneAndSendOtherBeacons) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Creates 5 beacons. Deletes the 3rd of them, and sends out the others.
  auto beacons = CreateBeacons(total, method);

  const size_t deleted_beacon_i = 2;
  beacons[deleted_beacon_i].remote->Deactivate();

  for (int i = beacons.size() - 1; i >= 0; i--) {
    if (i != deleted_beacon_i) {
      SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i));
    }
    beacons[i].SendNow();
  }
  ExpectTotalNetworkRequests(FROM_HERE, total - 1);
}

TEST_P(PendingBeaconHostTest, SendOnDocumentUnloadWithBackgroundSync) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Creates 5 beacons on the page.
  auto beacons = CreateBeacons(total, method);

  SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
                      blink::mojom::PermissionStatus::GRANTED);
  // Forces deleting the page where `host` resides.
  DeleteContents();

  ExpectTotalNetworkRequests(FROM_HERE, total);
}

TEST_P(PendingBeaconHostTest, SendOnDocumentUnloadWithoutBackgroundSync) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Creates 5 beacons on the page.
  auto beacons = CreateBeacons(total, method);

  SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
                      blink::mojom::PermissionStatus::ASK);
  // Forces deleting the page where `host` resides.
  DeleteContents();

  ExpectTotalNetworkRequests(FROM_HERE, total);
}

TEST_P(PendingBeaconHostTest, SendOnNavigation) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Creates 5 beacons on the page.
  auto beacons = CreateBeacons(total, method);

  // Simulates sends on pagehide.
  host()->SendAllOnNavigation();

  ExpectTotalNetworkRequests(FROM_HERE, total);
}

TEST_P(PendingBeaconHostTest, SendOnProcessExit) {
  const std::string method = GetParam();
  const size_t total = 5;

  // Creates 5 beacons on the page.
  auto beacons = CreateBeacons(total, method);

  // Simulates sending on process exits.
  ChildProcessTerminationInfo termination_info;
  host()->RenderProcessExited(main_rfh()->GetProcess(), termination_info);

  ExpectTotalNetworkRequests(FROM_HERE, total);
}

class BeaconTestBase : public PendingBeaconHostTestBase {
 protected:
  void TearDown() override {
    host_ = nullptr;
    PendingBeaconHostTestBase::TearDown();
  }

  scoped_refptr<network::ResourceRequestBody> CreateRequestBody(
      const std::string& data) {
    return network::ResourceRequestBody::CreateFromBytes(data.data(),
                                                         data.size());
  }

  scoped_refptr<network::ResourceRequestBody> CreateFileRequestBody(
      uint64_t offset = 0,
      uint64_t length = 10) {
    scoped_refptr<network::ResourceRequestBody> body =
        base::MakeRefCounted<network::ResourceRequestBody>();
    body->AppendFileRange(base::FilePath(FILE_PATH_LITERAL("file.txt")), offset,
                          length, base::Time());
    return body;
  }

  scoped_refptr<network::ResourceRequestBody> CreateComplexRequestBody() {
    auto body = CreateRequestBody("part1");
    body->AppendFileRange(base::FilePath(FILE_PATH_LITERAL("part2.txt")), 0, 10,
                          base::Time());
    return body;
  }

  scoped_refptr<network::ResourceRequestBody> CreateStreamingRequestBody() {
    mojo::PendingRemote<network::mojom::ChunkedDataPipeGetter> remote;
    auto unused_receiver = remote.InitWithNewPipeAndPassReceiver();
    scoped_refptr<network::ResourceRequestBody> body =
        base::MakeRefCounted<network::ResourceRequestBody>();
    body->SetToChunkedDataPipe(
        std::move(remote), network::ResourceRequestBody::ReadOnlyOnce(false));
    return body;
  }

  mojo::Remote<blink::mojom::PendingBeacon>& CreateBeaconAndPassRemote(
      const std::string& method) {
    beacon_ = CreateBeacon(method);
    return beacon_->remote;
  }

 private:
  // Owned by `main_rfh()`.
  raw_ptr<PendingBeaconHost> host_;
  std::unique_ptr<MockClientBeacon> beacon_;
};

struct BeaconURLTestType {
  const char* name;
  const char* url_;
  bool expect_supported;
  std::string url() const { return std::string(url_); }
};

// GURL will try to canonicalize invalid url strings.
constexpr BeaconURLTestType kBeaconURLTestCases[] = {
    {"HTTP_LOCALHOST_URL", "http://localhost", false},
    {"HTTPS_LOCALHOST_URL", "https://localhost", true},
    {"IP_URL", "127.0.0.1", false},
    {"HTTP_IP_URL", "http://127.0.0.1", false},
    {"HTTPS_IP_URL", "https://127.0.0.1", true},
    {"HTTP_URL", "http://example.com", false},
    {"HTTPS_URL", "https://example.com", true},
    {"FILE_URL", "file://tmp", false},
    {"SSH_URL", "ssh://example.com", false},
    {"ABOUT_BLANK_URL", "about:blank", false},
    {"JAVASCRIPT_URL", "javascript:alert('');", false},
};

class CreateBeaconTest : public BeaconTestBase,
                         public ::testing::WithParamInterface<
                             std::tuple<std::string, BeaconURLTestType>> {};

INSTANTIATE_TEST_SUITE_P(
    All,
    CreateBeaconTest,
    ::testing::Combine(::testing::Values(net::HttpRequestHeaders::kGetMethod,
                                         net::HttpRequestHeaders::kPostMethod),
                       ::testing::ValuesIn(kBeaconURLTestCases)),
    [](const ::testing::TestParamInfo<
        std::tuple<std::string, BeaconURLTestType>>& info) {
      return base::StrCat(
          {std::get<0>(info.param), "_", std::get<1>(info.param).name});
    });

TEST_P(CreateBeaconTest, CreateWithURL) {
  const std::string& method = std::get<0>(GetParam());
  const auto url = std::get<1>(GetParam()).url();
  const bool expect_supported = std::get<1>(GetParam()).expect_supported;

  // Intercepts Mojo bad-message error.
  std::string bad_message;
  mojo::SetDefaultProcessErrorHandler(
      base::BindLambdaForTesting([&](const std::string& error) {
        ASSERT_TRUE(bad_message.empty());
        bad_message = error;
      }));

  CreateBeacon(method, url);

  if (!expect_supported) {
    EXPECT_EQ(bad_message, "Unexpected url format from renderer");
  }
}

using GetBeaconTest = BeaconTestBase;

TEST_F(GetBeaconTest, AttemptToSetRequestDataForGetBeaconAndTerminated) {
  auto& beacon_remote =
      CreateBeaconAndPassRemote(net::HttpRequestHeaders::kGetMethod);
  // Intercepts Mojo bad-message error.
  std::string bad_message;
  mojo::SetDefaultProcessErrorHandler(
      base::BindLambdaForTesting([&](const std::string& error) {
        ASSERT_TRUE(bad_message.empty());
        bad_message = error;
      }));

  beacon_remote->SetRequestData(CreateRequestBody("data"), "");
  beacon_remote.FlushForTesting();

  EXPECT_EQ(bad_message, "Unexpected BeaconMethod from renderer");
}

TEST_F(GetBeaconTest, AttemptToSetRequestURLWithInvalidSchemeAndTerminated) {
  auto& beacon_remote =
      CreateBeaconAndPassRemote(net::HttpRequestHeaders::kGetMethod);

  for (const auto& test_case : kBeaconURLTestCases) {
    // Intercepts Mojo bad-message error.
    std::string bad_message;
    mojo::SetDefaultProcessErrorHandler(
        base::BindLambdaForTesting([&](const std::string& error) {
          ASSERT_TRUE(bad_message.empty());
          bad_message = error;
        }));

    beacon_remote->SetRequestURL(GURL(test_case.url()));
    beacon_remote.FlushForTesting();

    if (!test_case.expect_supported) {
      EXPECT_EQ(bad_message, "Unexpected url format from renderer")
          << test_case.name;
    }
  }
}

using PostBeaconTest = BeaconTestBase;

TEST_F(PostBeaconTest, AttemptToSetRequestDataWithComplexBodyAndTerminated) {
  auto& beacon_remote =
      CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
  // Intercepts Mojo bad-message error.
  std::string bad_message;
  mojo::SetDefaultProcessErrorHandler(
      base::BindLambdaForTesting([&](const std::string& error) {
        ASSERT_TRUE(bad_message.empty());
        bad_message = error;
      }));

  beacon_remote->SetRequestData(CreateComplexRequestBody(), "");
  beacon_remote.FlushForTesting();

  EXPECT_EQ(bad_message, "Complex body is not supported yet");
}

TEST_F(PostBeaconTest, AttemptToSetRequestDataWithStreamingBodyAndTerminated) {
  auto& beacon_remote =
      CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
  // Intercepts Mojo bad-message error.
  std::string bad_message;
  mojo::SetDefaultProcessErrorHandler(
      base::BindLambdaForTesting([&](const std::string& error) {
        ASSERT_TRUE(bad_message.empty());
        bad_message = error;
      }));

  beacon_remote->SetRequestData(CreateStreamingRequestBody(), "");
  beacon_remote.FlushForTesting();

  EXPECT_EQ(bad_message, "Streaming body is not supported.");
}

TEST_F(PostBeaconTest, AttemptToSetRequestURLForPostBeaconAndTerminated) {
  auto& beacon_remote =
      CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
  // Intercepts Mojo bad-message error.
  std::string bad_message;
  mojo::SetDefaultProcessErrorHandler(
      base::BindLambdaForTesting([&](const std::string& error) {
        ASSERT_TRUE(bad_message.empty());
        bad_message = error;
      }));

  beacon_remote->SetRequestURL(GURL("/test_set_url"));
  beacon_remote.FlushForTesting();

  EXPECT_EQ(bad_message, "Unexpected BeaconMethod from renderer");
}

class PostBeaconRequestDataTest : public BeaconTestBase {
 protected:
  // Registers a callback to verify if the most-recent network request's content
  // matches the given `expected_body` and `expected_content_type`.
  void SetExpectNetworkRequest(
      const base::Location& location,
      scoped_refptr<network::ResourceRequestBody> expected_body,
      const absl::optional<std::string>& expected_content_type =
          absl::nullopt) {
    test_url_loader_factory_->SetInterceptor(base::BindLambdaForTesting(
        [location, expected_body,
         expected_content_type](const network::ResourceRequest& request) {
          ASSERT_EQ(request.method, net::HttpRequestHeaders::kPostMethod)
              << location.ToString();
          ASSERT_EQ(request.request_body->elements()->size(), 1u)
              << location.ToString();

          const auto& expected_element = expected_body->elements()->at(0);
          const auto& element = request.request_body->elements()->at(0);
          EXPECT_EQ(element.type(), expected_element.type());
          if (expected_element.type() == network::DataElement::Tag::kBytes) {
            const auto& expected_bytes =
                expected_element.As<network::DataElementBytes>();
            const auto& bytes = element.As<network::DataElementBytes>();
            EXPECT_EQ(bytes.AsStringPiece(), expected_bytes.AsStringPiece())
                << location.ToString();
          } else if (expected_element.type() ==
                     network::DataElement::Tag::kFile) {
            const auto& expected_file =
                expected_element.As<network::DataElementFile>();
            const auto& file = element.As<network::DataElementFile>();
            EXPECT_EQ(file.path(), expected_file.path()) << location.ToString();
            EXPECT_EQ(file.offset(), expected_file.offset())
                << location.ToString();
            EXPECT_EQ(file.length(), expected_file.length())
                << location.ToString();
          }

          if (!expected_content_type.has_value()) {
            EXPECT_FALSE(request.headers.HasHeader(
                net::HttpRequestHeaders::kContentType))
                << location.ToString();
            return;
          }
          std::string content_type;
          EXPECT_TRUE(request.headers.GetHeader(
              net::HttpRequestHeaders::kContentType, &content_type))
              << location.ToString();
          EXPECT_EQ(content_type, expected_content_type) << location.ToString();
        }));
  }

  mojo::Remote<blink::mojom::PendingBeacon>& CreateBeaconAndPassRemote() {
    return BeaconTestBase::CreateBeaconAndPassRemote(
        net::HttpRequestHeaders::kPostMethod);
  }
};

TEST_F(PostBeaconRequestDataTest, SendBytesWithCorsSafelistedContentType) {
  auto& beacon_remote = CreateBeaconAndPassRemote();

  auto body = CreateRequestBody("data");
  beacon_remote->SetRequestData(body, "text/plain");

  SetExpectNetworkRequest(FROM_HERE, body, "text/plain");
  beacon_remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_F(PostBeaconRequestDataTest, SendBytesWithEmptyContentType) {
  auto& beacon_remote = CreateBeaconAndPassRemote();

  auto body = CreateRequestBody("data");
  beacon_remote->SetRequestData(body, "");

  SetExpectNetworkRequest(FROM_HERE, body);
  beacon_remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_F(PostBeaconRequestDataTest, SendBlobWithCorsSafelistedContentType) {
  auto& beacon_remote = CreateBeaconAndPassRemote();

  auto body = CreateFileRequestBody();
  beacon_remote->SetRequestData(body, "text/plain");

  SetExpectNetworkRequest(FROM_HERE, body, "text/plain");
  beacon_remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_F(PostBeaconRequestDataTest, SendBlobWithEmptyContentType) {
  auto& beacon_remote = CreateBeaconAndPassRemote();

  auto body = CreateFileRequestBody();
  beacon_remote->SetRequestData(body, "");

  SetExpectNetworkRequest(FROM_HERE, body);
  beacon_remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

TEST_F(PostBeaconRequestDataTest, SendBlobWithNonCorsSafelistedContentType) {
  auto& beacon_remote = CreateBeaconAndPassRemote();

  auto body = CreateFileRequestBody();
  beacon_remote->SetRequestData(body, "application/unsafe");

  SetExpectNetworkRequest(FROM_HERE, body, "application/unsafe");
  beacon_remote->SendNow();
  ExpectTotalNetworkRequests(FROM_HERE, 1);
}

}  // namespace content
